Chapter 8 — S&P 500 (Python supplement)¶
Condensed notebook for the Python portion of Chapter 8: S&P 500 Data.
Target graphics:
- Indexed S&P 500 performance by month since inauguration (Figure 8.14; book target Figure 8.1)
Data: S&P 500 - Historical Data.csv in Data For Condensed Notebooks.
Dependencies:
pandasnumpyplotly
Imports and display options¶
In [1]:
# Import packages.
import pandas as pd
import numpy as np
import plotly.express as px
In [2]:
# Avoid SettingWithCopy warnings.
pd.options.mode.copy_on_write = True
/var/folders/_x/nbw2t83x75d0lr3jn4pg_cx00000gn/T/ipykernel_26650/3183164771.py:2: Pandas4Warning: The 'mode.copy_on_write' option is deprecated. Copy-on-Write can no longer be disabled (it is always enabled with pandas >= 3.0), and setting the option has no impact. This option will be removed in pandas 4.0. pd.options.mode.copy_on_write = True
In [3]:
# Set the maximum number of DataFrame rows to display.
pd.options.display.max_rows = 8
In [4]:
# Set output options.
import plotly.io as pio
pio.renderers.default = "pdf+jupyterlab+notebook"
Loading and cleaning the data¶
In [5]:
df = pd.read_csv('../Data For Condensed Notebooks/S&P 500 - Historical Data.csv')
df
Out[5]:
| Date | Close/Last | Open | High | Low | President | |
|---|---|---|---|---|---|---|
| 0 | 2/7/25 | 6025.99 | 6083.13 | 6101.28 | 6019.96 | NaN |
| 1 | 2/6/25 | 6083.57 | 6072.22 | 6084.03 | 6046.83 | NaN |
| 2 | 2/5/25 | 6061.48 | 6020.45 | 6062.86 | 6007.06 | NaN |
| 3 | 2/4/25 | 6037.88 | 5998.14 | 6042.48 | 5990.87 | NaN |
| ... | ... | ... | ... | ... | ... | ... |
| 2519 | 2/12/15 | 2088.48 | 2069.98 | 2088.53 | 2069.98 | NaN |
| 2520 | 2/11/15 | 2068.53 | 2068.55 | 2073.48 | 2057.99 | NaN |
| 2521 | 2/10/15 | 2068.59 | 2049.38 | 2070.86 | 2048.62 | NaN |
| 2522 | 2/9/15 | 2046.74 | 2053.47 | 2056.16 | 2041.88 | NaN |
2523 rows × 6 columns
In [6]:
df.dtypes
Out[6]:
Date str Close/Last float64 Open float64 High float64 Low float64 President str dtype: object
Parse dates; keep only Biden and Trump rows (query).
In [7]:
df['Date'] = pd.to_datetime(df['Date'])
df = df.query('President == "Biden" | President == "Trump"')
df
/var/folders/_x/nbw2t83x75d0lr3jn4pg_cx00000gn/T/ipykernel_26650/3575661020.py:1: UserWarning: Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format. df['Date'] = pd.to_datetime(df['Date'])
Out[7]:
| Date | Close/Last | Open | High | Low | President | |
|---|---|---|---|---|---|---|
| 14 | 2025-01-17 | 5996.66 | 5995.40 | 6014.96 | 5978.44 | Biden |
| 15 | 2025-01-16 | 5937.34 | 5963.61 | 5964.69 | 5930.72 | Biden |
| 16 | 2025-01-15 | 5949.91 | 5905.21 | 5960.61 | 5905.21 | Biden |
| 17 | 2025-01-14 | 5842.91 | 5859.27 | 5871.92 | 5805.42 | Biden |
| ... | ... | ... | ... | ... | ... | ... |
| 2020 | 2017-01-26 | 2296.68 | 2298.63 | 2300.99 | 2294.08 | Trump |
| 2021 | 2017-01-25 | 2298.37 | 2288.88 | 2299.55 | 2288.88 | Trump |
| 2022 | 2017-01-24 | 2280.07 | 2267.88 | 2284.63 | 2266.68 | Trump |
| 2023 | 2017-01-23 | 2265.20 | 2267.78 | 2271.78 | 2257.02 | Trump |
2010 rows × 6 columns
Calculations¶
Hard-code term start dates, compute months since start, average Open by president and month, then index to 100 at month 0.
In [8]:
biden_start_date = pd.to_datetime('2021/01/21')
trump_start_date = pd.to_datetime('2017/01/23')
In [9]:
df.loc[:, 'Start Date'] = np.where(df['President'] == 'Biden',
biden_start_date, trump_start_date)
In [10]:
month_offset = df['Date'].dt.to_period('M') - df['Start Date'].dt.to_period('M')
df.loc[:, 'Months Since Start'] = month_offset.apply(lambda offset: offset.n)
df.head(50)
Out[10]:
| Date | Close/Last | Open | High | Low | President | Start Date | Months Since Start | |
|---|---|---|---|---|---|---|---|---|
| 14 | 2025-01-17 | 5996.66 | 5995.40 | 6014.96 | 5978.44 | Biden | 2021-01-21 | 48 |
| 15 | 2025-01-16 | 5937.34 | 5963.61 | 5964.69 | 5930.72 | Biden | 2021-01-21 | 48 |
| 16 | 2025-01-15 | 5949.91 | 5905.21 | 5960.61 | 5905.21 | Biden | 2021-01-21 | 48 |
| 17 | 2025-01-14 | 5842.91 | 5859.27 | 5871.92 | 5805.42 | Biden | 2021-01-21 | 48 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 60 | 2024-11-08 | 5995.54 | 5976.76 | 6012.45 | 5976.76 | Biden | 2021-01-21 | 46 |
| 61 | 2024-11-07 | 5973.10 | 5947.21 | 5983.84 | 5947.21 | Biden | 2021-01-21 | 46 |
| 62 | 2024-11-06 | 5929.04 | 5864.89 | 5936.14 | 5864.89 | Biden | 2021-01-21 | 46 |
| 63 | 2024-11-05 | 5782.76 | 5722.43 | 5783.44 | 5722.10 | Biden | 2021-01-21 | 46 |
50 rows × 8 columns
Group by president and month; take mean Open; merge starting values for indexing.
In [11]:
df_by_month = df.groupby(['President', 'Months Since Start']).agg({'Open': 'mean'})
df_by_month
Out[11]:
| Open | ||
|---|---|---|
| President | Months Since Start | |
| Biden | 0 | 3826.710000 |
| 1 | 3878.793158 | |
| 2 | 3906.684783 | |
| 3 | 4134.019048 | |
| ... | ... | ... |
| Trump | 45 | 3423.191364 |
| 46 | 3543.430000 | |
| 47 | 3692.005455 | |
| 48 | 3780.282500 |
98 rows × 1 columns
In [12]:
biden_start_open = df_by_month.loc['Biden', 0]['Open']
trump_start_open = df_by_month.loc['Trump', 0]['Open']
start_open_df = pd.DataFrame({'President': ['Biden', 'Trump'],
'Start Open': [biden_start_open, trump_start_open]})
df_by_month.reset_index(inplace=True)
df_by_month = pd.merge(df_by_month, start_open_df, on=['President'])
df_by_month['Indexed Open'] = (df_by_month['Open'] / df_by_month['Start Open']) * 100
df_by_month
Out[12]:
| President | Months Since Start | Open | Start Open | Indexed Open | |
|---|---|---|---|---|---|
| 0 | Biden | 0 | 3826.710000 | 3826.710000 | 100.000000 |
| 1 | Biden | 1 | 3878.793158 | 3826.710000 | 101.361043 |
| 2 | Biden | 2 | 3906.684783 | 3826.710000 | 102.089910 |
| 3 | Biden | 3 | 4134.019048 | 3826.710000 | 108.030633 |
| ... | ... | ... | ... | ... | ... |
| 94 | Trump | 45 | 3423.191364 | 2283.174286 | 149.931233 |
| 95 | Trump | 46 | 3543.430000 | 2283.174286 | 155.197526 |
| 96 | Trump | 47 | 3692.005455 | 2283.174286 | 161.704933 |
| 97 | Trump | 48 | 3780.282500 | 2283.174286 | 165.571351 |
98 rows × 5 columns
Figure 8.14 — Indexed performance line chart (solid Biden, dashed Trump).
In [13]:
px.line(df_by_month,
x = 'Months Since Start',
y = 'Indexed Open',
color = 'President',
line_dash= 'President',
line_dash_map={"Biden": "solid", "Trump": "dash"},
title = 'S&P 500 Performance - Biden vs Trump (1st Term)')